use crate::{Solution, SolutionPair};
use std::cmp::PartialEq;
use std::fs::read_to_string;

enum GameResult {
    Win,
    Loss,
    Draw,
}

impl GameResult {
    fn calc_score(&self, shape: &GameShape) -> u32 {
        let result_score = match self {
            Self::Loss => 0,
            Self::Draw => 3,
            Self::Win => 6,
        };

        result_score + shape.get_score()
    }

    fn new(strategy: &str) -> Self {
        match strategy {
            "X" => Self::Loss,
            "Y" => Self::Draw,
            "Z" => Self::Win,
            _ => panic!("❗️[GameResult]: error strategy input"),
        }
    }
}

#[derive(PartialEq)]
enum GameShape {
    Rock,
    Paper,
    Scissor,
}

impl GameShape {
    fn new(strategy: &str) -> Self {
        match strategy {
            "X" | "A" => Self::Rock,
            "Y" | "B" => Self::Paper,
            "Z" | "C" => Self::Scissor,
            _ => panic!("❗️[GameShape]: error strategy input"),
        }
    }

    fn compare(&self, opponent: &Self) -> GameResult {
        match (self, opponent) {
            (Self::Rock, Self::Rock) => GameResult::Draw,
            (Self::Paper, Self::Paper) => GameResult::Draw,
            (Self::Scissor, Self::Scissor) => GameResult::Draw,
            (Self::Rock, Self::Scissor) => GameResult::Win,
            (Self::Scissor, Self::Paper) => GameResult::Win,
            (Self::Paper, Self::Rock) => GameResult::Win,
            (Self::Scissor, Self::Rock) => GameResult::Loss,
            (Self::Paper, Self::Scissor) => GameResult::Loss,
            (Self::Rock, Self::Paper) => GameResult::Loss,
        }
    }

    fn get_shape_from(opponent: &Self, game_result: &GameResult) -> Self {
        match (opponent, game_result) {
            (Self::Rock, GameResult::Loss) => Self::Scissor,
            (Self::Rock, GameResult::Draw) => Self::Rock,
            (Self::Rock, GameResult::Win) => Self::Paper,
            (Self::Paper, GameResult::Loss) => Self::Rock,
            (Self::Paper, GameResult::Draw) => Self::Paper,
            (Self::Paper, GameResult::Win) => Self::Scissor,
            (Self::Scissor, GameResult::Loss) => Self::Paper,
            (Self::Scissor, GameResult::Draw) => Self::Scissor,
            (Self::Scissor, GameResult::Win) => Self::Rock,
        }
    }

    fn get_score(&self) -> u32 {
        match self {
            Self::Rock => 1,
            Self::Paper => 2,
            Self::Scissor => 3,
        }
    }
}

pub fn solve() -> SolutionPair {
    let input = read_to_string("input/day02.txt").unwrap();

    let sol1: u32 = part1(process_input1(String::from(&input)));
    let sol2: u32 = part2(process_input2(String::from(&input)));

    (Solution::from(sol1), Solution::from(sol2))
}

fn process_input1(input: String) -> Vec<(GameShape, GameShape)> {
    input
        .lines()
        .map(|line| {
            let strategies = line.split(' ').collect::<Vec<&str>>();
            let opponent = GameShape::new(strategies[0]);
            let mine = GameShape::new(strategies[1]);

            (opponent, mine)
        })
        .collect::<Vec<(GameShape, GameShape)>>()
}

fn part1(strategy_pairs: Vec<(GameShape, GameShape)>) -> u32 {
    strategy_pairs.iter().fold(0, |result, (opponent, mine)| {
        result + mine.compare(opponent).calc_score(mine)
    })
}

fn process_input2(input: String) -> Vec<(GameShape, GameResult)> {
    input
        .lines()
        .map(|line| {
            let strategies = line.split(' ').collect::<Vec<&str>>();
            let opponent = GameShape::new(strategies[0]);
            let result = GameResult::new(strategies[1]);

            (opponent, result)
        })
        .collect::<Vec<(GameShape, GameResult)>>()
}

fn part2(strategy_pairs: Vec<(GameShape, GameResult)>) -> u32 {
    strategy_pairs
        .iter()
        .fold(0, |result, (opponent, game_result)| {
            let mine = GameShape::get_shape_from(opponent, game_result);
            result + game_result.calc_score(&mine)
        })
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::etc::utils;

    #[test]
    fn test_part1() {
        assert_eq!(
            15,
            part1(process_input1(utils::to_multiline_string(vec![
                "A Y", "B X", "C Z"
            ])))
        );
    }

    #[test]
    fn test_part2() {
        assert_eq!(
            12,
            part2(process_input2(utils::to_multiline_string(vec![
                "A Y", "B X", "C Z"
            ])))
        );
    }
}
